﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

//First published July, 2011 by Adam Hill
//http://pathcompressor.codeplex.com
//Feedback & ideas are very welcome

namespace Org.AdamHill.Sl.Tools.PathCompressor
{
    public static class PathCompressor
    {
        /// <summary>
        /// Takes a XAML document & compresses the Data attribute of any Paths by rounding their values to a specified precision
        /// </summary>
        /// <param name="XamlDocument">The XAML document to be processed, in string form</param>
        /// <param name="DecimalPlaces">The number of decimal places to round path values to (0-2 recommended)</param>
        /// <returns>The processed XAML document as a string</returns>
        public static string CompressXamlDocument(string XamlDocument, int DecimalPlaces)
        {
            StringBuilder sb = new StringBuilder();
            List<char> xamlChars = XamlDocument.ToCharArray().ToList();
            int startPointer = 0;
            int endPointer = 0;
            bool isProcessingPath = false;
            bool hasHitDataDeclaration = false;
            bool isMeasuringDataValue = false;
            for (int i = 0; i < xamlChars.Count; i++)
            {
                char c = xamlChars[i];
                if (!isProcessingPath)
                {
                    if (xamlChars[i] == '<')
                    {
                        string pathOpenTagCheck = RangeToString(xamlChars, i, i + 4);
                        if (pathOpenTagCheck.ToUpper() == "<PATH")//XAML = InitCaps, SVG = AllCaps, ToUpper-ed for safety (first SVG I came across was all lowercase)
                        {
                            isProcessingPath = true;
                        }
                    }
                    sb.Append(xamlChars[i]);
                }
                else
                {
                    if (!hasHitDataDeclaration)
                    {
                        if (xamlChars[i] == 'D')
                        {
                            if (i < xamlChars.Count - 2)//avoid index out of bounds
                            {
                                if (RangeToString(xamlChars, i, i + 3) == "Data")//XAML Path Data attribute
                                {
                                    hasHitDataDeclaration = true;
                                }
                            }
                        }
                        else if (xamlChars[i] == 'd')
                        {
                            if (i > 0 && i < xamlChars.Count)//avoid index out of bounds
                            {
                                if (RangeToString(xamlChars, i - 1, i + 1) == " d=")//SVG PATH d (data) attribute
                                {
                                    hasHitDataDeclaration = true;
                                }
                            }
                        }
                        sb.Append(xamlChars[i]);
                    }
                    else
                    {
                        if (!isMeasuringDataValue)
                        {
                            if (xamlChars[i] == '"')
                            {
                                isMeasuringDataValue = true;
                                startPointer = i + 1;
                                endPointer = i + 1;
                            }
                            if (xamlChars[i] == '>')
                            {
                                isProcessingPath = false;
                            }
                            sb.Append(xamlChars[i]);
                        }
                        else
                        {
                            if (xamlChars[i] == '"')
                            {
                                isMeasuringDataValue = false;
                                hasHitDataDeclaration = false;
                                sb.Append(CompressXamlPathData(RangeToString(xamlChars, startPointer, endPointer + 1), DecimalPlaces));
                            }
                            else
                            {
                                endPointer = i;
                            }
                        }
                    }
                }
            }
            return sb.ToString();
        }

        #region Data Compression
        /// <summary>
        /// Processes the raw data of a Path's Data attribute, rounding each number to a precision factor as a simple form of lossy compression
        /// </summary>
        /// <param name="XamlPathData">The raw Path Data as a string</param>
        /// <returns>The processed raw Path Data as a string</returns>
        public static string CompressXamlPathData(string XamlPathData, int DecimalPlaces)
        {
            XamlPathData = FixDoubleDot(XamlPathData);
            XamlPathData = FixDoubleSpace(XamlPathData);
            return FixMissingZero(RoundDoubleValues(XamlPathData, DecimalPlaces));
        }

        static string RoundDoubleValues(string XamlPathData, int DecimalPlaces)
        {
            StringBuilder sb = new StringBuilder();
            int startPointer = 0;
            int endPointer = 0;
            bool isProcessingADouble = false;
            List<char> list = XamlPathData.ToCharArray().ToList();
            for (int i = 0; i < list.Count; i++)
            {
                char c = list[i];
                if (isProcessingADouble)
                {
                    if (IsNumeric(c) || IsDecimalPlace(c))
                    {
                        endPointer = i;
                    }
                    if ((!IsNumeric(c) && !IsDecimalPlace(c)) || i == list.Count - 1)
                    {
                        sb.Append(RoundedRangeToString(list, startPointer, endPointer, DecimalPlaces));
                        if (!IsNumeric(c) && !IsDecimalPlace(c))
                        {
                            sb.Append(c);
                        }
                        isProcessingADouble = false;
                    }
                }
                else
                {
                    if (IsNumeric(c) || IsDecimalPlace(c))
                    {
                        isProcessingADouble = true;
                        startPointer = i;
                        endPointer = i;
                    }
                    else
                    {
                        sb.Append(c);
                    }
                }
            }
            return sb.ToString();
        }
        #endregion /Data Compression

        #region String Helpers
        static string RangeToString(List<char> list, int StartPointer, int EndPointer)
        {
            StringBuilder sb = new StringBuilder();
            for (int j = StartPointer; j <= EndPointer; j++)
            {
                sb.Append(list[j]);
            }
            return sb.ToString();
        }

        static string RoundedRangeToString(List<char> list, int StartPointer, int EndPointer, int DecimalPlaces)
        {
            StringBuilder sb = new StringBuilder();
            for (int j = StartPointer; j <= EndPointer; j++)
            {
                sb.Append(list[j]);
            }
            string s = Convert.ToString(sb);
            if (IsScientificNotation(s.ToList()))//check for Scientific Notation added 2011/08/04
            {
                return sb.ToString();
            }
            else
            {
                bool isNumber;
                double number;
                isNumber = Double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out number);
                return isNumber ? Math.Round(number, DecimalPlaces).ToString() : sb.ToString();
            }
        }

        static bool PrefixWithDelimiterIfNotFirstToken(bool IsFirstToken, StringBuilder StringBuilderObject, string Delimiter)
        {
            if (!IsFirstToken)
                StringBuilderObject.Append(Delimiter);
            return false;
        }

        static string FixDoubleDot(string Text)
        {
            return Text.Replace("..", ".0,0.");
        }

        static string FixMissingZero(string Text)
        {
            string result = Text.Replace("M,", "M0,");
            result = result.Replace("L,", "L0,");
            result = result.Replace("H,", "H0,");
            result = result.Replace("V,", "V0,");
            result = result.Replace("C,", "C0,");
            result = result.Replace("Q,", "Q0,");
            result = result.Replace("S,", "S0,");
            result = result.Replace("T,", "T0,");
            result = result.Replace("A,", "A0,");
            //lowercase added for SVG support...
            result = result.Replace("m,", "m0,");
            result = result.Replace("l,", "l0,");
            result = result.Replace("h,", "h0,");
            result = result.Replace("v,", "v0,");
            result = result.Replace("c,", "c0,");
            result = result.Replace("q,", "q0,");
            result = result.Replace("s,", "s0,");
            result = result.Replace("t,", "t0,");
            result = result.Replace("a,", "a0,");
            return result;
        }

        static string FixDoubleSpace(string Text)
        {
            return Text.Replace("  ", " ");
        }
        #endregion /String Helpers

        #region Character Content Helpers
        //oh, for regex knowledge
        static bool IsNumeric(char c)
        {
            return new List<char> { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'e' }.Contains(c);//e for Scientific Notation, changed 2011/08/04
        }

        static bool IsScientificNotation(List<char> list)//added 2011/08/04
        {
            return list.Contains('e');
        }

        static bool IsDecimalPlace(char c)
        {
            return new List<char> { '.' }.Contains(c);
        }

        static bool IsSignPrefix(char c)
        {
            return new List<char> { '+', '-' }.Contains(c);
        }

        static bool IsSpecialCharacter(char c)
        {
            //Infinity, NaN, Space
            return new List<char> { 'I', 'n', 'f', 'i', 'n', 't', 'y', 'N', 'a', ' ' }.Contains(c);
        }

        static bool IsInstruction(char c)
        {
            //either case of Z is valid
            //lowercase of other instructions is for SVG
            return new List<char> { 'M', 'F', 'L', 'V', 'H', 'C', 'Q', 'S', 'T', 'A', 'Z', 'm', 'f', 'l', 'v', 'h', 'c', 'q', 's', 't', 'a', 'z' }.Contains(c);
        }

        public static string InvalidChars(string StringToTest)
        {
            StringBuilder sb = new StringBuilder();
            foreach (var c in StringToTest.ToCharArray())
            {
                if (IsOtherChar(c))
                {
                    sb.Append(c);
                }
            }

            return sb.ToString();
        }

        static bool IsOtherChar(char c)
        {
            return !(IsInstruction(c) || IsSpecialCharacter(c) || IsSignPrefix(c) || IsDecimalPlace(c) || IsNumeric(c) || c == ',');
        }
        #endregion /Character Content Helpers
    }
}